# forward-compat boilerplate
from __future__ import absolute_import
from __future__ import with_statement
__metaclass__ = type

import json
import urlparse
import os.path as op
import cPickle as pickle

import webob
import webob.exc
from weberror.errormiddleware import ErrorMiddleware

from fenton import app
from fenton import util
from fenton import types
from fenton import getmeta
from fenton import logging
from fenton import security


LONGTIMEAGO=-10**9
CHUNK_SIZE = 4096 * 16
DEFAULT_ENCODING = 'UTF-8'


def stack(app):

    if app.debug:
        print app

    config = app.config
    config.setdefault('beaker.session.key', 's')
    config.setdefault('beaker.session.secret', 'fenton-very-secret')
    config['beaker.session.data_dir'] = config['work_dir']
    encrypt_key = config.get('fenton.encrypt_key')
    sign_key  = config.get('fenton.sign_key')

    if sign_key:
        signer = security.get_signer(sign_key)
    else:
        logging.log.info('no sign key, not signing session')
        signer = None

    if encrypt_key:
        crypter = security.get_crypter(encrypt_key)
    else:
        logging.log.info('no encrypt key, not encrypting session')
        crypter = None

    dumper = Dumper(Pickler, signer, crypter)
    renderer = mako_renderer(app)
    stack = shell = Shell(app, renderer, dumper)
    stack = error_middleware(stack, config)
    stack = session_middleware(stack, config)
    stack = clean_cookies(stack)

    if not config.get('static_prefix'):
        from paste.cascade import Cascade
        from paste.urlparser import StaticURLParser
        dirs = [op.join(d, 'static') for d in app.source_dirs()]
        statics = [StaticURLParser(d) for d in dirs]
        stack = Cascade(statics + [stack])

    if shell.prefix:
        stack = prefix_middleware(stack, shell.prefix)

    stack = force_https(stack)
    stack = zap_favicon(stack)
    delay = config.get('delay_response', 0)
    if delay:
        stack = delay_response(stack, float(delay))
    return stack


def debug_request(rq):
    url = rq.url[len(rq.host_url):]
    from fenton import console
    parts = ['%s %s' % (rq.method, console.colors.Green(url))]
    for name, value in sorted(rq.headers.items()):
        parts.append('%s: %s' % (console.colors.LightGray(name), value))
    logging.log.debug('\n'.join(parts))


def clean_cookies(app):
    def _app(environ, start_response):
        cookie = environ.get('HTTP_COOKIE')
        if cookie:
            #logging.log.debug('cookies: %s', cookie)
            environ['HTTP_COOKIE'] = cookie.replace(':', '__colon__')
        return app(environ, start_response)
    return _app


def zap_favicon(app):
    def _app(environ, start_response):
        if environ['PATH_INFO'] == '/favicon.ico':
            start_response('404 Not found',
                           [('content-type', 'text/plain'), ('Content-Length', '0')])
            return []
        return app(environ, start_response)
    return _app


def delay_response(app, s=1):
    def _app(environ, start_response):
        import time, random
        time.sleep(random.random() * s)
        return app(environ, start_response)
    return _app


def force_https(app):
    def _app(env, resp):
        if env.get('HTTP_X_FORCE_HTTPS'):
            env['wsgi.url_scheme'] = 'https'
        return app(env, resp)
    return _app


def session_middleware(*args, **kw):
    from beaker.middleware import SessionMiddleware
    return SessionMiddleware(*args, **kw)


def prefix_middleware(app, prefix):
    import re
    reg = re.compile('^'+prefix)
    pre_len = len(prefix)
    def _app(env, start_response):
        url = env['PATH_INFO']
        if url == '/' or not url:
            start_response('301 Elsewhere', [('Location', prefix)])
            return ['Location: ' + prefix]
        if reg.match(url):
            env['SCRIPT_NAME'] = prefix
            env['PATH_INFO'] = reg.sub('', url) or '/'
        return app(env, start_response)
    return _app


def error_middleware(app, config):
    E = config.get('debug') and debug_middleware or ErrorNotifier
    return E(app, config)


def debug_middleware(app, config):
    from weberror.evalexception import EvalException
    app = EvalException(app, config, xmlhttp_key=Request.xmlhttp_key,
                        templating_formatters=[format_mako_error])
    from fenton import console
    g = console.colors.LightGray

    def debug_response(env, start_response):
        def new_start_response(status, headers, exc_info=None):
            lines = ['%s: %s' % (g(k), v) for (k,v) in sorted(headers)]
            logging.log.debug('%s\n%s\n', status, '\n'.join(lines))
            rq = env.get('fenton.request')
            if rq and rq.XHR:
                print rq.response.body
            return start_response(status, headers, exc_info)
        return new_start_response

    def _app(env, start_response):
        if not env['PATH_INFO'].startswith('/_debug/'):
            start_response = debug_response(env, start_response)
        return app(env, start_response)
    return _app




class ErrorNotifier(ErrorMiddleware):
    def __init__(self, app, config):
        default_message = 'An internal server error occurred'
        kw = {}
        kw['debug'] = config.get('debug') or False
        kw['error_email'] = config['email_to']
        kw['smtp_server'] = config['smtp_server']
        kw['from_address'] = config['error_email_from']
        kw['error_subject_prefix'] = config.get('error_subject_prefix',
                                                   'Web error: ')
        kw['error_message'] = config.get('error_message', default_message)
        kw['error_log'] = config.get('error_log', None)
        super(ErrorNotifier, self).__init__(app, config, **kw)

    def exception_handler(self, exc_info, environ):
        discard = super(ErrorNotifier, self).exception_handler(exc_info, environ)
        request = environ['fenton.request']
        if request.XHR:
            request.response.headers['content-type'] = 'application/json'
            return json.dumps({'error': 'server error'})
        else:
            from fenton import widgets
            return request.render(widgets.server_error())


def mako_renderer(app):
    from mako.lookup import TemplateLookup
    from mako.runtime import Namespace
    Namespace.__repr__ = lambda x: '<Namespace %s>' % x.name
    config = app.config
    dirs = [op.join(d, 'templates') for d in app.source_dirs()]
    work_dir = op.join(config['work_dir'], 'templates')
    return TemplateLookup(directories=dirs,
                          input_encoding=DEFAULT_ENCODING,
                          output_encoding=config.get('www_encoding',
                                                     DEFAULT_ENCODING),
                          imports=['import markupsafe'],
                          default_filters=['markupsafe.escape_silent'],
                          error_handler=mako_errorhandler,
                          module_directory=not app.debug and work_dir or None)


def mako_errorhandler(context, e):
    import sys
    t = sys.exc_info()[2]
    while t.tb_next:
        t = t.tb_next
    f = t.tb_frame.f_globals.get('__file__')
    if f and 'mako' in f:
        try:
            e.__mako = True
        except:
            pass
    raise


def format_mako_error(e):
    '''Format a Mako exception as HTML'''
    import mako.exceptions as mx
    if hasattr(e, '__mako') or isinstance(e, mx.MakoException):
        template = mx.html_error_template()
        return template.render(full=False, css=True, error=e)





class Response(webob.Response):
    def __delete_cookie(self, key, path='/', domain=None):
        self.set_cookie(key, '', path=path,
                        domain=domain,
                        max_age=LONGTIMEAGO)
    @property
    def user_headers(self):
        return [(k, v) for (k, v) in self.headerlist
                    if k.lower() == 'set-cookie'
                        or k[:2].lower() == 'x-']




class Request(app.Request):

    xmlhttp_key = '__xhr'
    response_class = Response
    session_key = 'beaker.session'
    default_charset = DEFAULT_ENCODING
    context_maxlen = 10
    heartbeat_seconds = 300
    context_expire_seconds = 900

    app = property(lambda x:x.shell.app)
    dumper = property(lambda x:x.shell.dumper)
    config = property(lambda x:x.shell.app.config)

    __session = None
    @property
    def session(self):
        if self.session_key in self.environ:
            return self.environ[self.session_key]
        if not self.__session:
            self.__session = {}
        return self.__session

    def __init__(self, environ, shell):
        self.environ = environ
        self.shell = shell
        self.charset = (shell.charset or self.default_charset)

        self.__request = webob.Request(environ, charset=self.charset)
        self.MSIE = 'MSIE' in self.headers.get('user-agent', '')
        self.XHR = self.headers.get('x-requested-with', '').lower() == 'xmlhttprequest'

    def __getattr__(self, k):
        return getattr(self.__request, k)

    def __repr__(self):
        return '<%s at 0x%x [%r]>' % (self.__class__.__name__,
                                      id(self),
                                      self.__request)

    def __str__(self):
        return str(self.__request)

    def timer(self):
        t = self.environ['fenton.started']
        return util.timer() - t

    def log(self, msg, *args):
        t = '[%0.1f] ' % (self.timer() * 1000)
        logging.log.debug(t+msg, *args)

    @property
    def tz(self):
        offset = self.session.get('tz')
        if offset is None:
            logging.log.debug('no tz in session, getting from cookie')
            offset = self.cookies.get('tzoffset')
            self.session['tz'] = offset
        if offset is None:
            logging.log.debug('no tzoffset in cookie!')
            return self.app.tz
        #if not offset:
        #    offset = self.params.get('____tzoffset')
        from fenton import timestamp
        return offset and timestamp.FixedOffset(-int(offset)) or None

    @util.lazyattr
    def context_id(self):
        path = self.path_info and self.path_info[1:]
        if path and path.startswith(self.context_prefix):
            return path[len(self.context_prefix):]

    @util.lazyattr
    def vars(self):
        from fenton import form
        vars = util.varsobject()
        vars.update(form.decode(self.__request.params.mixed()))
        ct = self.content_type.lower()
        if 'text/javascript' in ct or 'application/json' in ct:
            body = self.body
            if body:
                try:
                    obj = json.loads(body)
                except ValueError, e:
                    logging.log.error('JSON decoding error: %s, %s', e, body)
                    raise
                vars.update(obj)
        return vars

    @util.lazyattr
    def action(self):
        action = self.__request.params.get(self.bind_key, None)
        if not action and self.bind_key in self.vars:
            action = self.vars[self.bind_key]
            action = action and [x[0] for x in action.items() if x[1]]
            action = action and action[0]
        self.vars.pop(self.bind_key, None)
        return action

    __response = None
    @property
    def response(self):
        if self.__response is None:
            self.__response = self.response_class()
            self.__response.charset = self.charset
        return self.__response

    @property
    def model_vars(self):
        return self.vars.get(self.model_key, {})

    @property
    def messages(self):
        return self.session.setdefault('messages', {})

    def refresh(self, screen):
        url = screen.get_url()
        if url:
            self.messages[url] = screen.screen_message
        else:
            url = self.context_location(screen.context)
        self.redirect(url)

    def render(self, widget):
        if self.app.debug or not widget.cachable:
            cc = self.response.cache_control
            cc.no_store = cc.no_cache = cc.must_revalidate = True
            self.response.expires = 0
            self.response.pragma = 'no-cache'

        path = self.get_template_file(widget)
        tmod = self.shell.renderer.get_template(path)
        tdef = tmod.get_def(widget.template_name)
        return tdef.render(widget, request=self)

    def get_template_file(self, widget):
        return '/%s.mako' % widget.template_file.replace('.', '/')

    def update(self, request):
        self.__response = request.response
        self.vars.update(request.vars)
        self.user = request.user
        self.session.update(request.session)

    def context_location(self, context):
        return self.app_url('%s%s' % (self.context_prefix, context.id))

    def get_history(self):
        return self.session.setdefault(self.history_key, [])

    def get_referrer(self):
        if self.referer:
            scheme, netloc, path, query, fragment = urlparse.urlsplit(self.url)
            if self.host == netloc and path.startswith(self.shell.prefix):
                path = path[len(self.shell.prefix)+1:]
                if path and not path.startswith(self.context_prefix):
                    e = self.environ.copy()
                    e['PATH_INFO'] = path
                    e['QUERY_STRING'] = query
                    ref = self.shell.new_request(e)
                    return (path, ref.model_vars)

    @property
    def stored_contexts(self):
        try:
            return self.session.setdefault(self.context_key, {})
        except:
            logging.log.error('Error getting contexts')
            logging.log.exception()
            return {}

    def decode_context(self):
        id = self.context_id
        if id not in self.stored_contexts:
            logging.log.error('context %s not found in session %s', id, self.session.id)
            return None
        t, context = self.stored_contexts[id]
        if self.dumper:
            context = self.dumper.loads(context)
        if context is None:
            logging.log.error('Failed loading context from session')
            return None
        context.thaw(self)
        return context

    def store_context(self, context):
        self.purge_contexts()
        id = context.id
        if self.dumper:
            context = self.dumper.dumps(context)
        self.stored_contexts[id] = util.timer(), context

    def purge_contexts(self, max_age=None, max_len=None):
        if max_age is None:
            max_age = self.context_expire_seconds
        if max_len is None:
            max_len = self.context_maxlen

        if len(self.stored_contexts) <= max_len:
            return

        for id, (t, _) in self.stored_contexts.items():
            if id == self.context_id:
                continue
            t = util.timer() - t
            if t > max_age:
                del self.stored_contexts[id]

    @util.property
    def user():
        def fget(self):
            return self.session.get(self.user_key) or security.ANONYMOUS

        def fset(self, u):
            self.session[self.user_key] = u

    def abort(self, code, **kw):
        Ex = webob.exc.status_map[code]
        ex = Ex(headers=self.response.user_headers, **kw).exception
        ex.body = ' %s ' % code
        raise ex

    def redirect(self, url, code=302):
        if not url:
            raise ValueError('empty location')
        if not urlparse.urlsplit(url).scheme:
            url = self.app_url(url)
        if self.XHR:
            self.response.headers['x-location'] = str(url)
        else:
            self.abort(code=code, location=url)

    def absolute_url(self, url):
        if urlparse.urlparse(url).scheme:
            return url
        return self.host_url + self.app_url(url)

    def app_url(self, url):
        if url and url[0] == '/':
            return url
        prefix = self.shell.prefix
        if prefix and not url.startswith(prefix):
            url = ('/'.join((prefix, url))).replace('//', '/')
        return url

    def static_url(self, url):
        if urlparse.urlparse(url).scheme:
            return url
        prefix = self.config.get('static_prefix')
        if prefix:
            return '%s/%s' % (prefix, url)
        else:
            return self.app_url(url)

    def redirect_login(self, url=None):
        import urllib
        url = urllib.quote_plus(url or self.url)
        self.redirect(self.app.get_login_url(url))

    def new(self, **kw):
        old = urlparse.urlsplit(self.url)
        parts ='scheme', 'netloc', 'path', 'query', 'fragment'
        new = [kw.get(part) or getattr(old.part) for part in parts]
        return urlparse.urlunsplit(new)




class Closing:
    def __init__(self, it, close):
        self.it = it
        self._close = close
    def close(self):
        return self._close()
    def __iter__(self):
        return iter(self.it)




class BaseHandler:

    request = property(lambda x:x.context.request)
    invoked = False

    def handle(self):
        self.check()
        self.invoke()
        self.invoked = True
        self.render()

    def check(self):
        try:
            ok = security.check(self.priv, self.context, None)
        except security.RecheckWithObject:
            ok = security.check(self.priv, self.context, self.obj)
        if not ok:
            logging.log.error('security failed: %s %s', self.priv, self.context.user)
            raise security.NotAllowed

    def render(self):
        if not self.context.screen:
            raise AssertionError('Nothing to render')
        if self.request.XHR:
            self.request.response.headers['content-type'] = 'application/json'
            rsp = json.dumps(self.context.screen.vars)
        elif self.request.method == 'POST' and self.request.context_id:
            self.request.redirect(self.request.url)
        else:
            rsp = self.context.screen.render()
        self.context.screen.vars = {}
        if rsp is not None:
            self.request.response.body = rsp

    def finish(self):
        self.request.store_context(self.context)




class NewScreenHandler(BaseHandler):

    priv = property(lambda x:x.meta.get_priv('view'))

    def __init__(self, context, meta, arg):
        self.context = context
        self.meta = meta
        self.arg = arg

    def __repr__(self):
        if self.invoked:
            return 'Got::%s' % self.context.screen
        else:
            return 'Get::%s::view' % self.meta

    __obj = None
    @property
    def obj(self):
        if self.__obj is None:
            obj = self.meta.reconstruct(self.arg, self.context)
            self.__obj = obj
            self.meta = getmeta(obj)
        return self.__obj

    def invoke(self):
        cx = self.context
        screen = getmeta(self.obj).get_view(cx, self.obj, cx.screen)
        if screen is None:
            raise security.NotAllowed
        cx.screen = screen
        args = cx.request.model_vars
        if args:
            screen.update(args)
        cx.request.add_history(screen)
        screen.referer = cx.request.get_referrer()
        url = screen.get_url()
        screen.screen_message = cx.request.messages.pop(url, None)




class CurrentScreenHandler(BaseHandler):
    obj = property(lambda x:x.context.screen.model)
    priv = property(lambda x:x.context.screen.priv)

    def __init__(self, context):
        self.context = context

    def __repr__(self):
        return 'Re-Get::%s' % self.context.screen

    def invoke(self):
        pass




class BindingHandler(CurrentScreenHandler):
    def __init__(self, context, f, x, y):
        self.context = context
        self.f = f
        self.x = x
        self.y = y

    def __repr__(self):
        x = ', '.join(map(repr, self.x))
        y = ', '.join('%s=%r'%p for p in self.y.iteritems())
        z = ', '.join((x and [x] or []) + (y and [y] or []))
        f = self.f
        if hasattr(f, 'func_name'):
            f = '%s.%s' % (f.__module__, f.__name__)
        return '%s(%s)' % (f, z)

    def invoke(self):
        self.thaw_args()
        return self.f(*self.x, **self.y)

    def thaw_args(self):
        objs = list(self.x or []) + (self.y or {}).values()
        for x in objs:
            m = getmeta(x, False)
            if m and not isinstance(x, type):
                m.thaw_model(self.context, x)




class DebugInfo:
    def __init__(self, request, handler=None):
        self.request = request
        self.env = request.environ
        self.user = request.user
        self.action = request.action
        self.handler = handler

    def extraData(self):
        k = ('important', 'Fenton vars')
        return {k: {'user': self.user,
                    'handler': self.handler and str(self.handler)}}




class Shell(app.Shell):

    def __init__(self, app, renderer, dumper=None):
        self.app = app
        self.renderer = renderer
        self.dumper = dumper
        self.prefix = app.config.get('url_prefix')
        self.charset = app.config.get('www_charset')or Request.default_charset

    def __call__(self, environ, start_response):
        environ['fenton.started'] = util.timer()
        environ['fenton.shell'] = self
        environ['fenton.app'] = self.app
        environ['fenton.request'] = request = self.new_request(environ)

        if 0 and self.debug:
            debug_request(request)
        else:
            url = request.url.replace(request.host_url, '')
            logging.log.info('%s %s', request.method, url)
            if request.params:
                logging.log.info('%s', request.params)

        try:
            response = self.respond(request)
        except webob.exc.HTTPException, e:
            response = e
        except:
            logging.log.exception()
            raise
        finally:
            # must be here and not in closer so session is written
            # to storage before iterating app response
            request.session.save()

        if response is None:
            raise AssertionError('No response')
        response.headers['X-Execution-Time'] = '%.1f ms' % (request.timer() * 1000)
        return Closing(response(environ, start_response), request.close)

    def new_request(self, environ):
        return Request(environ, self)

    def respond(self, request):
        __traceback_supplement__ = DebugInfo, request
        handler = self._get_handler(request)
        __traceback_supplement__ += (handler,)

        if handler is None:
            return self._handle_not_found(request)

        close = handler.context.close

        try:
            handler.handle()
            request.close = close
        except security.NotAuthenticated:
            self._handle_not_authenticated(request)
        except security.NotAllowed:
            self._handle_not_allowed(request)
        except types.Invalid, e:
            if self.debug:
                raise
            logging.log.exception()
            self._handle_not_found(request)
        finally:
            handler.finish()
            if not self.debug:
                request.close = close
        return request.response

    def _handle_not_authenticated(self, request):
        request.redirect_login(request.url)

    def _handle_not_allowed(self, request):
        request.abort(401)

    def _handle_not_found(self, request):
        request.abort(404)

    def _get_handler(self, request):
        if not request.context_id:
            return self._resolve(request)
        if request.method == 'GET':
            return self._continued_as_get(request)
        if request.method == 'POST':
            return self._continued_as_post(request)

    def _resolve(self, rq):
        path = rq.path_info and rq.path_info[1:]
        tmp = self.app.resolve(path)
        if tmp:
            return tmp(rq.new_context())
        try:
            meta, arg = self.app.get(path)
        except KeyError:
            return None
        return NewScreenHandler(rq.new_context(), meta, arg)

    def _continued_as_get(self, request):
        context = request.decode_context()
        if not context:
            return None
        return CurrentScreenHandler(context)

    def _continued_as_post(self, request):
        if not request.action:
            logging.log.error('POST to context and no action!')
            return None

        context = request.decode_context()
        if not context:
            logging.log.error('Failed decoding context')
            return None

        binding = context.screen.get_binding(request.action)
        if not binding:
            logging.log.error('screen has no binding: %s', request.action)
            return None

        return BindingHandler(context, *binding)




class Pickler:
    dumps = staticmethod(lambda x:pickle.dumps(x, protocol=-1))
    loads = staticmethod(pickle.loads)




class Dumper:
    def __init__(self, serializer=Pickler, signer=None, crypter=None):
        self.serializer = serializer
        self.crypter = crypter
        self.signer = signer

    def dumps(self, obj):
        data = self.serializer.dumps(obj)
        if self.crypter:
            data = self.crypter.encrypt(data)
        if self.signer:
            data = self.signer.sign(data)
        return data

    def loads(self, data):
        if self.signer:
            data = self.signer.verify(data)
        if self.crypter:
            data = self.crypter.decrypt(data)
        return self.serializer.loads(data)




